SQL Server APPLY Basics

Comments 0

Share to social media

Starting with SQL Server 2005, you can use the APPLY operator in a Transact-SQL query to join a table to a table-valued function so the function is evoked for each row returned from the table. For example, you might create a query that returns a list of employees from the Employee table. For each employee, you also want to return a list of the individual job positions that person has held in the company.

You can create a function that retrieves the employees’ positions and then evoke that function for each row returned from the Employee table. Your result set will then include a row for each position that an employee has held. For instance, if John has worked as a salesman and a regional supervisor, the result set will include two rows for John, one for each position. Any data returned from the Employee table will be repeated for each row, but the data returned by the function will be specific to each row in the function’s results.

The APPLY operator can take one of two forms: CROSS APPLY or OUTER APPLY. The CROSS APPLY operator returns rows from the primary (outer) table only if the table-value function produces a result set. That means, in the example above, an employee would be included in the returned data only if that employee has held a specific position within the company. The OUTER APPLY form, on the other hand, returns all rows from the outer table, even if the function produces no results. So an employee would be listed even if that employee held no specific position.

In this article, I demonstrate how to work with both forms of the APPLY operator. The examples I show you were created on a local instance of SQL Server 2008 and the AdventureWorks2008 sample database. If you want to run these examples against the AdventureWorks database on an instance of either SQL Server 2005 or 2008, you must change references to the BusinessEntityID column to SalesPersonID column, where appropriate. In addition, you’ll find that your results might vary slightly from those shown here because the values between the databases are slightly different, particularly primary key values.

Using the CROSS APPLY Operator

As I mentioned, CROSS APPLY returns only those rows in the outer table for which the table value function returns data. Let’s look at an example to demonstrate how this works. Fist, we’ll create a function that returns the top three sales generated by a salesperson, as those sales appear in the Sales.SalesOrderHeader table in the AdventureWorks2008 database:

As you can see, the fn_sales function takes one parameter, @SalesPersonID, which is configured with the int data type. The function returns the three highest sales for the specified salesperson. Note that this is a table-valued function, which means that it returns the entire result set generated by the SELECT statement. For the fn_sales function, the result set includes the SalesPersonID and SalesAmount columns.

After you create your function, you can test it by running a SELECT statement that retrieves data from the function. For example, the following SELECT statement returns the SalesAmount column for salesperson ID 285:

The following table shows the results returned by the statement. As you would expect, three rows have been returned-the three highest sales for this salesperson.

SalesAmount

85652.33

45338.76

36317.54

After you’ve verified that the function is returning the correct results, you can use the function within a statement that includes the CROSS APPLY operator. In the following example, I retrieve data from the Sales.vSalesPerson view and join it to the fn_sales function:

As you can see, I use the CROSS APPLY operator in the FROM clause by first specifying the outer table (vSalesPerson), then the CROSS APPLY operator, and finally the fn_sales function. Notice that I pass in the BusinessEntityID value as the function’s parameter. This value is based on the BusinessEntityID value as it appears in the current row that is being returned from the outer table.

NOTE: The BusinessEntityID column in the vSalesPerson view uses the same IDs that are used in the SalesPersonID column in the SalesOrderHeader table. In the original version of the AdventureWorks database, the column name in vSalesPerson is SalesPersonID, just like it is in the SalesOrderHeader table.

Also notice that the columns in the SELECT list reference the source table and function as they would if I were joining two tables. I assign an alias to the table (sp) and one to the function (fn) and then reference the columns accordingly. For example, because the SalesAmount column in the SELECT list is returned by the function, I qualify the column name as fn.SalesAmount.

The following table shows part of the results returned by the SELECT statement above. Notice that each salesperson is listed three times, once for each result returned by the fn_sales function.

FullName

SalesAmount

Syed Abbas

85652.33

Syed Abbas

45338.76

Syed Abbas

36317.54

Amy Alberts

98405.08

Amy Alberts

96243.80

Amy Alberts

95193.67

Pamela Ansman-Wolfe

125254.49

Pamela Ansman-Wolfe

125144.01

Pamela Ansman-Wolfe

118284.78

Michael Blythe

198628.31

Michael Blythe

142942.01

Michael Blythe

139659.67

David Campbell

149897.36

David Campbell

136046.44

David Campbell

125068.34

Jillian Carson

162629.75

Jillian Carson

154912.07

Jillian Carson

144355.88

Shu Ito

247913.91

Shu Ito

227737.72

Shu Ito

189198.62

Stephen Jiang

149861.07

Stephen Jiang

114361.94

Stephen Jiang

112733.70

Tete Mensa-Annan

140734.49

Tete Mensa-Annan

137108.39

Tete Mensa-Annan

115068.64

That’s all there is to using the CROSS APPLY operator. Not let’s look at another example that uses the operator with a common table expression (CTE).

Using the CROSS APPLY Operator with a CTE

To demonstrate how you can use the APPLY operator with a CTE, I created the following function, which returns the product model associated with the specified product:

The function joins the Production.Product and Production.ProductModel tables to return the product name and model name for the specified product ID. I then used the following SELECT statement to verify that the function works as I expect:

The statement returns the product name and model for product 707 (shown in the following table). As you can see, the product Sport-100 Helmet, Red is associated with the model Sport-100.

ProductName

ModelName

Sport-100 Helmet, Red

Sport-100

You can then use the CROSS APPLY operator to join a CTE to the function, as shown in the following example:

Notice that I first define a CTE named ProductSales. The CTE returns the total sales for each product as they appear in the Sales.SalesOrderDetail table. I then use the CTE in the main SELECT statement, along with the fn_products function. Notice that I use the CROSS APPLY operator in the FROM clause to join the CTE to the function, as I would use the operator to join a table or view to the function. The following table shows part of the results returned by the SELECT statement.

ProductID

TotalSales

ProductModel

707

157772.394392

Sport-100

708

160869.517836

Sport-100

709

6060.388200

Mountain Bike Socks

710

513.000000

Mountain Bike Socks

711

165406.617049

Sport-100

712

51229.445623

Cycling Cap

713

21445.710000

Long-Sleeve Logo Jersey

714

115249.214976

Long-Sleeve Logo Jersey

715

198754.975360

Long-Sleeve Logo Jersey

716

95611.197080

Long-Sleeve Logo Jersey

717

394255.572400

HL Road Frame

718

395182.699300

HL Road Frame

719

89872.173600

HL Road Frame

722

177635.904000

LL Road Frame

723

24844.692200

LL Road Frame

725

194692.599104

LL Road Frame

726

132125.252200

LL Road Frame

727

20104.443400

LL Road Frame

729

195933.409400

LL Road Frame

730

137213.485128

LL Road Frame

732

89224.500000

ML Road Frame

733

32120.820000

ML Road Frame

736

45164.684600

LL Road Frame

738

299595.522966

LL Road Frame

739

269874.009600

HL Mountain Frame

741

141635.100000

HL Mountain Frame

742

499556.572400

HL Mountain Frame

743

901590.233600

HL Mountain Frame

744

13765.920000

HL Mountain Frame

745

106078.560000

HL Mountain Frame

One thing you might notice about the results shown here, compared to the results shown in the preceding example, is that the function returns only one row for each product. Because a product is associated with only one product model in the SalesOrderDetail table, there will never be more than one row per product.

Using the OUTER APPLY Operator

In the examples above, the CROSS APPLY operator returns the rows in the outer table for which the table value function returns data. As it turns out, the functions used in both examples return data for all rows in the outer tables. However, in some cases, the function will not return data for a specific row. If you still want the row from the outer table to be included in the result set, you should use the OUTER APPLY operator.

The OUTER APPLY operator returns all rows from the outer table, whether or not the function returns data for a specific row. You use the OUTER APPLY operator just as you would CROSS APPLY; the difference is in the results. Let’s look at an example that demonstrates how this works.

In the following statement, I create a function that returns data about product inventory:

This statement, just like the CREATE FUNCTION statements used in the preceding examples, creates a table-valued function. The function returns three columns: ProductID, LocationID, and Quantity. I verified the function by using the following SELECT statement:

In this case, I specified the product ID of 915 as the function’s parameter. The following table shows the results returned by the statement.

LocationID

Quantity

6

161

50

83

60

158

As you can see, product inventory exists in three locations, with different quantities at each location. Once the function has been verified, you can use it with a CROSS APPLY operator, as shown in the following statement:

Notice that this statement is similar to the previous examples. I’ve simply used the CROSS APPLY operator to join the Product table to the fn_inventory function. The following table shows part of the data returned by the statement. As the results indicate, there are multiple rows for each product, one for each quantity.

ProductID

Name

Quantity

915

ML Touring Seat/Saddle

161

915

ML Touring Seat/Saddle

83

915

ML Touring Seat/Saddle

158

916

HL Touring Seat/Saddle

425

916

HL Touring Seat/Saddle

288

916

HL Touring Seat/Saddle

276

921

Mountain Tire Tube

286

921

Mountain Tire Tube

243

922

Road Tire Tube

264

922

Road Tire Tube

241

923

Touring Tire Tube

262

923

Touring Tire Tube

240

928

LL Mountain Tire

240

928

LL Mountain Tire

369

929

ML Mountain Tire

385

929

ML Mountain Tire

284

930

HL Mountain Tire

267

930

HL Mountain Tire

232

The results shown in the table are those returned for product IDs 915 through 930. As you would expect, each row includes data returned from both the table and the function. Now let’s look at an example that uses OUTER APPLY, rather than CROSS APPLY:

This statement is exactly like the previous one, except for the APPLY operator. However, the results are slightly different, as shown in the following table:

ProductID

Name

Quantity

915

ML Touring Seat/Saddle

161

915

ML Touring Seat/Saddle

83

915

ML Touring Seat/Saddle

158

916

HL Touring Seat/Saddle

425

916

HL Touring Seat/Saddle

288

916

HL Touring Seat/Saddle

276

917

LL Mountain Frame – Silver, 42

NULL

918

LL Mountain Frame – Silver, 44

NULL

919

LL Mountain Frame – Silver, 48

NULL

920

LL Mountain Frame – Silver, 52

NULL

921

Mountain Tire Tube

286

921

Mountain Tire Tube

243

922

Road Tire Tube

264

922

Road Tire Tube

241

923

Touring Tire Tube

262

923

Touring Tire Tube

240

924

LL Mountain Frame – Black, 42

NULL

925

LL Mountain Frame – Black, 44

NULL

926

LL Mountain Frame – Black, 48

NULL

927

LL Mountain Frame – Black, 52

NULL

928

LL Mountain Tire

240

928

LL Mountain Tire

369

929

ML Mountain Tire

385

929

ML Mountain Tire

284

930

HL Mountain Tire

267

930

HL Mountain Tire

232

The result set now includes rows for products 917 through 920 and products 924 through 927, which were not included in the previous results. Notice that the Quantity column shows a NULL value for each of the new rows. By using the OUTER APPLY operator, we’re able to return all rows from the outer table, whether or not the function returns any rows.

As you can see, the APPLY operator can be a useful tool when you want to evoke a table-valued function for each row returned by a table expression (the outer table). You simply use the operator to join the outer table to the function. If you want to include only those rows from the outer table for which the function returns data, use the CROSS APPLY operator. If you want to return all rows from the outer table, regardless of whether or not the function returns data for a row, use the OUTER APPLY operator. For more details about either form of the APPLY operator and to see additional examples, check out the topic “Using APPLY” in SQL Server Books Online.

Load comments

About the author

Robert Sheldon

See Profile

Robert is a freelance technology writer based in the Pacific Northwest. He’s worked as a technical consultant and has written hundreds of articles about technology for both print and online publications, with topics ranging from predictive analytics to 5D storage to the dark web. He’s also contributed to over a dozen books on technology, developed courseware for Microsoft’s training program, and served as a developmental editor on Microsoft certification exams. When not writing about technology, he’s working on a novel or venturing out into the spectacular Northwest woods.